/**   
 * Copyright 2011 COMERE Harold
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package etrl.core;

import etrl.vecmath.Vector3f;
import etrl.vecmath.Vector4f;

/**
 * \class BoundingSphere
 * \brief Represents a bounding sphere with basic operations.
 * @author harold
 *
 */
public class BoundingSphere
{
	/**
	 * The center coordinates of the bounding sphere.
	 */
	private Vector4f _center;
	
	/**
	 * The radius of the bounding sphere.
	 */
	private float _radius;
	
	/**
	 * \brief Initializes an empty bounding sphere.
	 */
	public BoundingSphere()
	{
		_center = new Vector4f(0.f, 0.f, 0.f, 1.f);
		_radius = 0.f;
	}
	
	/**
	 * \brief Initializes a bounding sphere with the given center coordinates and radius.
	 * @param[in] center Coordinates of the center to set.
	 * @param[in] radius Radius to set.
	 */
	public BoundingSphere(Vector4f center, float radius)
	{
		_center = new Vector4f(center);
		_radius = radius;
	}
	
	/**
	 * \brief Clones the bounding sphere.
	 * @return Cloned bounding sphere.
	 */
	public BoundingSphere clone()
	{
		return new BoundingSphere(_center, _radius);
	}
	
	/**
	 * \brief Gets the coordinates of the center.
	 * @return center coords.
	 */
	public Vector4f getCenter()
	{
		return new Vector4f(_center);
	}
	
	/**
	 * \brief Gets the radius.
	 * @return radius.
	 */
	public float getRadius()
	{
		return _radius;
	}
	
	/**
	 * \brief Sets new coordinates for the center.
	 * @param[in] center New coordinates to set.
	 */
	public void setCenter(Vector4f center)
	{
		_center = new Vector4f(center);
	}
	
	/**
	 * \brief Sets a new radius.
	 * @param[in] radius New radius to set.
	 */
	public void setRadius(float radius)
	{
		_radius = radius;
	}
	
	/**
	 * \brief Checks if the given ray intersects the bounding sphere.
	 * @param[in] start Coordinates of the start point of the ray.
	 * @param[in] end Coordinates of the end point of the ray.
	 * @return true if the ray instersects or is included into the bounding sphere, else false.
	 */
	public boolean intersectsRay(Vector4f start, Vector4f end)
	{	    
		// First check if the two points are included into the sphere.
		// So compute distance between each point and sphere's center
		Vector3f distStartVect = new Vector3f(start.x, start.y, start.z);
		distStartVect.sub(new Vector3f(_center.x, _center.y, _center.z));
		float distStart = distStartVect.length();
		
		Vector3f distEndVect = new Vector3f(end.x, end.y, end.z);
		distEndVect.sub(new Vector3f(_center.x, _center.y, _center.z));
		float distEnd = distEndVect.length();	
		
		// So if both dist are < to radius, they are both in ! So the ray "intersects" the sphere.
		// If one is included into the sphere, and not the second one, the sphere is intersected for sure
		if(distStart < _radius || distEnd < _radius)
			return true;
		else
		{
			// Else here the two points are out of the sphere, so check if there is at least one intersection with the sphere
		    float alpha = (end.x - start.x) * (end.x - start.x) + (end.y - start.y) * (end.y - start.y) + (end.z - start.z) * (end.z - start.z);
		    float beta  = 2 * ((end.x - start.x) * (start.x - _center.x) + (end.y - start.y) * (start.y - _center.y) + (end.z - start.z) * (start.z - _center.z));
		    float gamma = start.x * start.x + start.y * start.y + start.z * start.z + _center.x * _center.x + _center.y * _center.y + _center.z * _center.z - 2 * (start.x * _center.x + start.y * _center.y + start.z * _center.z) - _radius * _radius;
		    
		    return (beta * beta - 4 * alpha * gamma) >= 0;
		}
	}
	
	/**
	 * \brief Expands the bounding sphere with another bounding sphere. The resulting bounding sphere includes the two ones.
	 * @param[in] bsphere Bounding sphere to expand with.
	 */
	public void expandBy(BoundingSphere bsphere)
	{
		// If the given bsphere is not valid, do nothing
		if(!bsphere.isValid())
			return;
		
		// If this bounding sphere is not valid, initialize it with the given one
		if(! isValid())
		{
			_center = bsphere.getCenter();
			_radius = bsphere.getRadius();
			return;
		}
		
	    // Calculate the distance between the sphere centers   
	    Vector3f vect = new Vector3f(_center.x, _center.y, _center.z);
	    vect.sub(new Vector3f(bsphere.getCenter().x, bsphere.getCenter().y, bsphere.getCenter().z));
	    float dist = vect.length();
	    
	    // If the given sphere is already contained in this one, we do not need to do anything
	    if (dist + bsphere.getRadius() > _radius ) 
	    {
		    //  If the given sphere completely contains this one 
		    if(dist + _radius <= bsphere.getRadius())  
		    {
		    	// Copy the given sphere data
		        _center = bsphere.getCenter();
		        _radius = bsphere.getRadius();
		    }
		    else // else no one includes the other one
		    {
			    // Build a new sphere that completely contains others
			    float new_radius = (_radius + dist + bsphere.getRadius() ) * 0.5f;
			    float ratio = ( new_radius - _radius ) / dist ;
		
			    _center.x += vect.x * ratio;
			    _center.y += vect.y * ratio;
			    _center.z += vect.z * ratio;
		
			    _radius = new_radius;
		    }
	    }
	}
	
	/**
	 * \brief Expands the bounding sphere by the one including the given bounding box. 
	 * @param[in] bbox bounding box to expand with.
	 */
	public void expandBy(BoundingBox bbox)
	{
		expandBy(bbox.asBoundingSphere());
	}
	
	/**
	 * \brief Checks if the bounding sphere is valid.
	 * @return true if the bounding sphere is valid, else false.
	 */
	public boolean isValid()
	{
		return _radius != 0.f;
	}
}
